import { Directive, ElementRef, HostListener, Input, Output, EventEmitter, OnInit, OnDestroy, Renderer2, NgZone } from '@angular/core';
import { of as observableOf, Subscription, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { NgDragDropService } from '@app/core/services/common/drag-drop.service';
import { DomHelper } from '@app/utils/dom-helper';
import { NzSafeAny } from 'ng-zorro-antd/core/types';

export class DropEvent {
  nativeEvent: any;
  dragData: any;

  constructor(event: any, data: any) {
    this.nativeEvent = event;
    this.dragData = data;
  }
}

@Directive({
  selector: '[droppable]',
  standalone: true
})
export class DroppableDirective implements OnInit, OnDestroy {
  /**
   *  Event fired when Drag dragged element enters a valid drop target.
   */
  // eslint-disable-next-line @angular-eslint/no-output-on-prefix
  @Output() readonly onDragEnter: EventEmitter<any> = new EventEmitter();

  /**
   * Event fired when an element is being dragged over a valid drop target
   */
  // eslint-disable-next-line @angular-eslint/no-output-on-prefix
  @Output() readonly onDragOver: EventEmitter<any> = new EventEmitter();

  /**
   * Event fired when a dragged element leaves a valid drop target.
   */
  // eslint-disable-next-line @angular-eslint/no-output-on-prefix
  @Output() readonly onDragLeave: EventEmitter<any> = new EventEmitter();

  /**
   * Event fired when an element is dropped on a valid drop target.
   */
  // eslint-disable-next-line @angular-eslint/no-output-on-prefix
  @Output() readonly onDrop: EventEmitter<DropEvent> = new EventEmitter();

  /**
   * CSS class that is applied when a compatible draggable is being dragged over this droppable.
   */
  @Input() dragOverClass = 'drag-over-border';

  /**
   * CSS class applied on this droppable when a compatible draggable item is being dragged.
   * This can be used to visually show allowed drop zones.
   */
  @Input() dragHintClass = 'drag-hint-border';

  /**
   * Defines compatible drag drop pairs. Values must match both in draggable and droppable.dropScope.
   */
  @Input() dropScope: string | string[] | Function = 'default';

  /**
   * Defines if drop is enabled. `true` by default.
   */
  @Input() set dropEnabled(value: boolean) {
    this._dropEnabled = value;

    if (this._dropEnabled === true) {
      this.subscribeService();
    } else {
      this.unsubscribeService();
    }
  }

  get dropEnabled(): boolean {
    return this._dropEnabled;
  }

  /**
   * @private
   */
  dragStartSubscription!: Subscription;

  /**
   * @private
   */
  dragEndSubscription!: Subscription;

  /**
   * @private
   * Backing field for the dropEnabled property
   */
  _dropEnabled = true;

  /**
   * @private
   * Field for tracking drag state. Helps when
   * drag stop event occurs before the allowDrop()
   * can be calculated (async).
   */
  _isDragActive = false;

  /**
   * @private
   * Field for tracking if service is subscribed.
   * Avoids creating multiple subscriptions of service.
   */
  _isServiceActive = false;

  /**
   * @private
   * Function for unbinding the drag enter listener
   */
  unbindDragEnterListener!: Function;

  /**
   * @private
   * Function for unbinding the drag over listener
   */
  unbindDragOverListener!: Function;

  /**
   * @private
   * Function for unbinding the drag leave listener
   */
  unbindDragLeaveListener!: Function;

  constructor(
    protected el: ElementRef,
    private renderer: Renderer2,
    private ng2DragDropService: NgDragDropService,
    private zone: NgZone
  ) {}

  ngOnInit(): void {
    if (this.dropEnabled === true) {
      this.subscribeService();
    }
  }

  ngOnDestroy(): void {
    this.unsubscribeService();
    this.unbindDragListeners();
  }

  dragEnter(e: NzSafeAny): void {
    e.preventDefault();
    e.stopPropagation();
    this.onDragEnter.emit(e);
  }

  dragOver(e: NzSafeAny, result: NzSafeAny): void {
    if (result) {
      DomHelper.addClass(this.el, this.dragOverClass);
      e.preventDefault();
      this.onDragOver.emit(e);
    }
  }

  dragLeave(e: NzSafeAny): void {
    DomHelper.removeClass(this.el, this.dragOverClass);
    e.preventDefault();
    this.onDragLeave.emit(e);
  }

  @HostListener('drop', ['$event'])
  drop(e: NzSafeAny): void {
    this.allowDrop().subscribe(result => {
      if (result && this._isDragActive) {
        DomHelper.removeClass(this.el, this.dragOverClass);
        e.preventDefault();
        e.stopPropagation();

        this.ng2DragDropService.onDragEnd.next(null);
        this.onDrop.emit(new DropEvent(e, this.ng2DragDropService.dragData));
        this.ng2DragDropService.dragData = null;
        this.ng2DragDropService.scope = [];
      }
    });
  }

  allowDrop(): Observable<boolean> {
    let allowed: boolean | Observable<boolean> = false;

    /* tslint:disable:curly */
    /* tslint:disable:one-line */
    if (typeof this.dropScope === 'string') {
      if (typeof this.ng2DragDropService.scope === 'string') allowed = this.ng2DragDropService.scope === this.dropScope;
      else if (this.ng2DragDropService.scope instanceof Array) allowed = this.ng2DragDropService.scope.indexOf(this.dropScope) > -1;
    } else if (this.dropScope instanceof Array) {
      if (typeof this.ng2DragDropService.scope === 'string') allowed = this.dropScope.indexOf(this.ng2DragDropService.scope) > -1;
      else if (this.ng2DragDropService.scope instanceof Array)
        allowed =
          this.dropScope.filter(item => {
            return this.ng2DragDropService.scope.indexOf(item) !== -1;
          }).length > 0;
    } else if (typeof this.dropScope === 'function') {
      allowed = this.dropScope(this.ng2DragDropService.dragData);
      if (allowed instanceof Observable) {
        return allowed.pipe(map(result => result && this.dropEnabled));
      }
    }

    /* tslint:enable:curly */
    /* tslint:disable:one-line */

    return observableOf(allowed && this.dropEnabled);
  }

  subscribeService(): void {
    if (this._isServiceActive === true) {
      return;
    }
    this._isServiceActive = true;
    this.dragStartSubscription = this.ng2DragDropService.onDragStart.subscribe(() => {
      this._isDragActive = true;
      this.allowDrop().subscribe(result => {
        if (result && this._isDragActive) {
          DomHelper.addClass(this.el, this.dragHintClass);

          this.zone.runOutsideAngular(() => {
            this.unbindDragEnterListener = this.renderer.listen(this.el.nativeElement, 'dragenter', dragEvent => {
              this.dragEnter(dragEvent);
            });
            this.unbindDragOverListener = this.renderer.listen(this.el.nativeElement, 'dragover', dragEvent => {
              this.dragOver(dragEvent, result);
            });
            this.unbindDragLeaveListener = this.renderer.listen(this.el.nativeElement, 'dragleave', dragEvent => {
              this.dragLeave(dragEvent);
            });
          });
        }
      });
    });

    this.dragEndSubscription = this.ng2DragDropService.onDragEnd.subscribe(() => {
      this._isDragActive = false;
      DomHelper.removeClass(this.el, this.dragHintClass);
      this.unbindDragListeners();
    });
  }

  unsubscribeService(): void {
    this._isServiceActive = false;
    if (this.dragStartSubscription) {
      this.dragStartSubscription.unsubscribe();
    }
    if (this.dragEndSubscription) {
      this.dragEndSubscription.unsubscribe();
    }
  }

  unbindDragListeners(): void {
    if (this.unbindDragEnterListener) {
      this.unbindDragEnterListener();
    }
    if (this.unbindDragOverListener) {
      this.unbindDragOverListener();
    }
    if (this.unbindDragLeaveListener) {
      this.unbindDragLeaveListener();
    }
  }
}
